
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections;
using System.Web.Services.Description;
using System.Web.Services.Discovery;
using System.Web.Services.Protocols;
using Swaf.BizObj;
using Microsoft.CSharp;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Text;
using System.Net;
using System.Security.Cryptography;

namespace Swaf.Call
{
	#region Enumerations
	/// <summary>
	/// The supported protocols
	/// </summary>
	public enum ProtocolEnum
	{
		HttpGet,
		HttpPost,
		HttpSoap
	}
	#endregion

	/// <summary>
	/// Exposes the ability to invoke Web Services dynamically without 
	/// having to generate a client side proxy class
	/// </summary>
	public class DynamicWebServiceCall : DynamicCall
	{
		#region Constructors
		/// <summary>
		/// Constructor taking the WSDL Location
		/// </summary>
		public DynamicWebServiceCall(String wsdlLocation)
		{
			_wsdl = wsdlLocation;
			BuildProxy();
		}
		public DynamicWebServiceCall(IBizObj callDef) : base(callDef)
		{
			_wsdl = (string) callDef.get("extraInfo");
			_methodName = (string) callDef.get("methodName");
			base.m_methodName=MethodName;
			BuildProxy();
		}
		/// <summary>
		/// Constructor taking the WSDL Location and the Method Name
		/// </summary>
		public DynamicWebServiceCall(String wsdlLocation, String methodName)
		{
			_wsdl = wsdlLocation;
			_methodName = methodName;
			base.m_methodName = methodName;
			BuildProxy();
		}
		
		/// <summary>
		/// Constructor taking the WSDL Location, Method Name, and Method Params
		/// </summary>
		public DynamicWebServiceCall(String wsdlLocation, String methodName, params Object[] methodParams)
		{
			_wsdl = wsdlLocation;
			_methodName = methodName;
			_methodParams = null;
			_methodParams = new ArrayList(methodParams);
			BuildProxy();
		}
		#endregion

		#region Public Properties
		/// <summary>
		/// Property to get and set the URL
		/// </summary>
		public String Url
		{
			get
			{
				PropertyInfo propInfo = _proxyInstance.GetType().GetProperty(URL);
				Object result = propInfo.GetValue(_proxyInstance, null);
				
				return (String)result;
			}
			set
			{
				PropertyInfo propInfo = _proxyInstance.GetType().GetProperty(URL);
				propInfo.SetValue(_proxyInstance, value,
					BindingFlags.NonPublic | BindingFlags.Static |
					BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetField,
					null, null, null
					);
			}
		}
		
		/// <summary>
		/// Property to get and set the WSDL
		/// </summary>
		public String WSDL
		{
			get {return _wsdl;}
			set {_wsdl = value;}
		}
		
		/// <summary>
		/// Property to get and set the Type Name
		/// </summary>
		public String TypeName
		{
			get {return _typeName;}
			set {_typeName = value;}
		}

		/// <summary>
		/// Property to get and set the Method Name
		/// </summary>
		public String MethodName
		{
			get {return _methodName;}
			set {_methodName = value;}
		}

		/// <summary>
		/// Property to get and set the Protocol Name
		/// </summary>
		public ProtocolEnum ProtocolName
		{
			get
			{
				switch(_protocolName)
				{
					case HttpGet:
						return ProtocolEnum.HttpGet;
					case HttpPost:
						return ProtocolEnum.HttpPost;
					case Soap:
						return ProtocolEnum.HttpSoap;
					default:
						return ProtocolEnum.HttpSoap;
				}
			}
			set
			{
				switch(value)
				{
					case ProtocolEnum.HttpGet:
						_protocolName = HttpGet;
						break;
					case ProtocolEnum.HttpPost:
						_protocolName = HttpPost;
						break;
					case ProtocolEnum.HttpSoap:
						_protocolName = Soap;
						break;
				}
			}
		}

		/// <summary>
		/// Property to get the Method Parameters
		/// </summary>
		public ArrayList MethodParams
		{
			get {return _methodParams;}
		}
		
		/// <summary>
		/// Property to get the Soap Request
		/// </summary>
		public String SoapRequest
		{
			get
			{
				PropertyInfo propInfo = _proxyInstance.GetType().GetProperty("SoapRequest");
				Object result = propInfo.GetValue(_proxyInstance, null);
				System.Text.UTF8Encoding enc = new System.Text.UTF8Encoding();
				
				return enc.GetString((byte[])result);
			}
		}

		/// <summary>
		/// Property to get the Soap Response
		/// </summary>
		public String SoapResponse
		{
			get
			{
				PropertyInfo propInfo = _proxyInstance.GetType().GetProperty("SoapResponse");
				Object result = propInfo.GetValue(_proxyInstance, null);
				System.Text.UTF8Encoding enc = new System.Text.UTF8Encoding();
				
				return enc.GetString((byte[])result);
			}
		}

		/// <summary>
		/// Property to get the Proxy Assembly
		/// </summary>
		public Assembly ProxyAssembly
		{
			get {return _assem;}
		}
		#endregion

		#region Private Routines
		/// <summary>
		/// Get the Wsdl for the source
		/// </summary>
		private void GetWsdl(String source) 
		{
			// This could be a valid WSDL representation
			if(source.StartsWith("<?xml version") == true)
			{
				_wsdlSource = source;
				return;	
			}
			// This is a URL to the WSDL
			else if(source.StartsWith("http://") == true)
			{
				_wsdlSource = GetWsdlFromUri(source);
				return;
			}
							
			// Try to get from local file system
			_wsdlSource = GetWsdlFromFile(source);
			return;
		}
		
		/// <summary>
		/// Get the wsdl from the uri
		/// </summary>
		private String GetWsdlFromUri(String uri)
		{
			WebRequest req = WebRequest.Create(uri);
			WebResponse result = req.GetResponse();
					
			Stream ReceiveStream = result.GetResponseStream();
			Encoding encode = System.Text.Encoding.GetEncoding("utf-8");
			StreamReader sr = new StreamReader(ReceiveStream, encode);
					
			String wsdlSource = sr.ReadToEnd();
			sr.Close();
					
			return wsdlSource;
		}
		
		/// <summary>
		/// Get the wsdl from a file
		/// </summary>
		private String GetWsdlFromFile(String fileFullPathName)
		{
			FileInfo fi = new FileInfo(fileFullPathName);
					
			if(fi.Extension == "wsdl")
			{
				FileStream fs = new FileStream(fileFullPathName, FileMode.Open, FileAccess.Read);
				StreamReader sr = new StreamReader(fs);
							
				char[] buffer = new char[(int)fs.Length];
				sr.ReadBlock(buffer, 0, (int)fs.Length);
				sr.Close();
							
				return new String(buffer);
			}
				
			throw new Exception("This is not a WSDL file");
		}

		/// <summary>
		/// Generate assembly from the supplied wsdl
		/// </summary>
		private Assembly BuildAssemblyFromWsdl(String strWsdl)
		{
			// Use an XmlTextReader to get the Web Service description
			StringReader  wsdlStringReader = new StringReader(strWsdl);
			XmlTextReader tr = new XmlTextReader(wsdlStringReader);
			ServiceDescription sd = ServiceDescription.Read(tr);
			tr.Close();

			// WSDL service description importer 
			CodeNamespace cns = new CodeNamespace("Swaf.Call.DynamicProxy");
			_sdi = new ServiceDescriptionImporter();
			//sdi.AddServiceDescription(sd, null, null);
			
			// check for optional imports in the root WSDL
			CheckForImports(_wsdl);

			_sdi.ProtocolName = _protocolName;
			_sdi.Import(cns, null);

			// change the base class
			CodeTypeDeclaration ctDecl = cns.Types[0];
			cns.Types.Remove(ctDecl);
			ctDecl.BaseTypes[0] = new CodeTypeReference("Swaf.Call.SoapHttpClientProtocolEx");
			cns.Types.Add(ctDecl);

			// source code generation
			CSharpCodeProvider cscp = new CSharpCodeProvider();
			ICodeGenerator icg = cscp.CreateGenerator();
			StringBuilder srcStringBuilder = new StringBuilder();
			StringWriter sw = new StringWriter(srcStringBuilder);
			icg.GenerateCodeFromNamespace(cns, sw, null);
			_proxySource = srcStringBuilder.ToString();
			sw.Close();

			// assembly compilation
			CompilerParameters cp = new CompilerParameters();
			cp.ReferencedAssemblies.Add("System.dll");
			cp.ReferencedAssemblies.Add("System.Xml.dll");
			cp.ReferencedAssemblies.Add("System.Web.Services.dll");
			cp.ReferencedAssemblies.Add("System.Data.dll");
			cp.ReferencedAssemblies.Add(System.Reflection.Assembly.
				GetExecutingAssembly().Location);
	
			cp.GenerateExecutable = false;
			cp.GenerateInMemory = true;  //set to "false" if generating a file
			cp.IncludeDebugInformation = false; 

			ICodeCompiler icc = cscp.CreateCompiler();
			CompilerResults cr = icc.CompileAssemblyFromSource(cp, _proxySource);
			
			if(cr.Errors.Count > 0)
				throw new Exception(String.Format(@"Build failed: {0} errors", cr.Errors.Count));

			_assem = cr.CompiledAssembly;
			
			// rename temporary assembly in order to cache it for later use (IF GENERATING A FILE)
			//RenameTempAssembly(cr.PathToAssembly);
			 
			// create proxy instance
			_proxyInstance = CreateInstance(_typeName);
			
			return _assem;
		}
		
		/// <summary>
		/// Create a instance of the generated assembly
		/// </summary>
		private Object CreateInstance(String objTypeName) 
		{
			// check whether the type is already created or not
			if (objTypeName == "" || objTypeName == null)
			{
				foreach (Type ty in ProxyAssembly.GetTypes())
				{
					if(ty.BaseType == typeof(Swaf.Call.SoapHttpClientProtocolEx))
					{
						objTypeName = ty.Name;
						break;
					}
				}
			}
			Type t = _assem.GetType("Swaf.Call.DynamicProxy." + objTypeName);
			
			return Activator.CreateInstance(t);
		}
		
		/// <summary>
		/// Rename the temp assembly if it is being persisted to disk
		/// </summary>
		private void RenameTempAssembly(String pathToAssembly)
		{			
			String path = Path.GetDirectoryName(pathToAssembly);
			String newFilename = path + @"\" + GetMd5Sum(_wsdl) + TempFileDll;
			
			File.Copy(pathToAssembly, newFilename, true);
		}

		/// <summary>
		/// Reset the state
		/// </summary>
		private void ResetInternalState()
		{
			_typeName = "";
			_methodName = "";
			_protocolName = "Soap";
			_methodParams.Clear();
			_sdi = null;
		}

		/// <summary>
		/// Build the proxy for the service
		/// </summary>
		private void BuildProxy()
		{
			// inject SOAP extensions in (client side) ASMX pipeline
			InjectExtension(typeof(SoapMessageAccessClientExtension));
			
			//check cache first (IF GENERATING A FILE)
//			if (!CheckCache())
//			{
//				GetWsdl(_wsdl);
//				BuildAssemblyFromWsdl(_wsdlSource);
//			}
			
			// Do check to see if file exists build everytime
			GetWsdl(_wsdl);
			BuildAssemblyFromWsdl(_wsdlSource);

			base.m_onThis = _proxyInstance;
		}

		/// <summary>
		/// Check to see if the dll file for the assembly exists on the disk
		/// and should be used instead of generating a new one
		/// </summary>
		private bool CheckCache()
		{
			String path = Path.GetTempPath() + GetMd5Sum(_wsdl) + TempFileDll;

			if(File.Exists(path))
			{
				_assem = Assembly.LoadFrom(path);
				// create proxy instance
				_proxyInstance = CreateInstance(_typeName);

				return true;
			}
			
			return false;
		}
		
		/// <summary>
		/// Check for optional imports in the root WSDL
		/// </summary>
		private void CheckForImports(String baseWSDLUrl)
		{
			DiscoveryClientProtocol dcp = new DiscoveryClientProtocol();
			dcp.DiscoverAny(baseWSDLUrl);
			dcp.ResolveAll();

			foreach (Object osd in dcp.Documents.Values)
			{
				if (osd is ServiceDescription) _sdi.AddServiceDescription((ServiceDescription)osd, null, null);;
				if (osd is XmlSchema) _sdi.Schemas.Add((XmlSchema)osd);
			}
		}

		/// <summary>
		/// Inject SOAP extensions in (client side) ASMX pipeline
		/// </summary>
		private static void InjectExtension(Type extension)
		{
			Assembly assBase;
			Type webServiceConfig;
			Object currentProp;
			PropertyInfo propInfo;
			Object[] value;
			Type myType;
			Object[] objArray;
			Object myObj;
			FieldInfo myField;

			try
			{
				assBase = typeof(SoapExtensionAttribute).Assembly;
				webServiceConfig =
					assBase.GetType("System.Web.Services.Configuration.WebServicesConfiguration");

				if (webServiceConfig == null)
					throw new Exception("Error ...");

				currentProp = webServiceConfig.GetProperty("Current",
					BindingFlags.NonPublic | BindingFlags.Static |
					BindingFlags.Instance | BindingFlags.Public
					).GetValue(null, null);
				propInfo = webServiceConfig.GetProperty("SoapExtensionTypes",
					BindingFlags.NonPublic | BindingFlags.Static |
					BindingFlags.Instance | BindingFlags.Public
					);
				value = (Object[])propInfo.GetValue(currentProp, null);
				myType = value.GetType().GetElementType();
				objArray = (Object[])Array.CreateInstance(myType, (int)value.Length + 1);
				Array.Copy(value, objArray, (int)value.Length);

				myObj = Activator.CreateInstance(myType);
				myField = myType.GetField("Type",
					BindingFlags.NonPublic | BindingFlags.Static |
					BindingFlags.Instance | BindingFlags.Public
					);
				myField.SetValue(myObj, extension,
					BindingFlags.NonPublic | BindingFlags.Static |
					BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetField,
					null, null
					);
				objArray[(int)objArray.Length - 1] = myObj;
				propInfo.SetValue(currentProp, objArray,
					BindingFlags.NonPublic | BindingFlags.Static |
					BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty,
					null, null, null
					);
			}
			catch (Exception e)
			{
				//TODO: Handle Exception
				throw e;
			}
		}

		/// <summary>
		/// Create a hash from wsdl to be used in file name
		/// </summary>
		private String GetMd5Sum(String str)
		{
			// First we need to convert the String into bytes, which
			// means using a text encoder
			Encoder enc = System.Text.Encoding.Unicode.GetEncoder();

			// Create a buffer large enough to hold the String
			byte[] unicodeText = new byte[str.Length * 2];
			enc.GetBytes(str.ToCharArray(), 0, str.Length, unicodeText, 0, true);

			// Now that we have a byte array we can ask the CSP to hash it
			MD5 md5 = new MD5CryptoServiceProvider();
			byte[] result = md5.ComputeHash(unicodeText);

			// Build the final String by converting each byte
			// into hex and appending it to a StringBuilder
			StringBuilder sb = new StringBuilder();
			for (int i=0;i<result.Length;i++)
			{
				sb.Append(result[i].ToString("X2"));
			}

			return sb.ToString();
		}
		#endregion

		#region Private Members
		private Assembly _assem = null;
		private Object _proxyInstance = null;
		private String _wsdl;
		private String _wsdlSource;
		private String _typeName;
		private String _methodName;
		private String _protocolName = "Soap";
		private ArrayList _methodParams = new ArrayList();
		private String _proxySource;
		private ServiceDescriptionImporter _sdi;
		#endregion

		#region Private Constants
		private const String TempFileDll = "_Swaf_tmp.dll";
		private const String URL = "Url";
		private const String HttpGet = "HttpGet";
		private const String HttpPost = "HttpPost";
		private const String Soap = "Soap";
		#endregion
	}
}
